--[[
Advanced Portals (advanced_portals), A minetest add-on that allows certain users to create and place portals on their minetest world.

Copyright (c) 2020 Genshin <emperor_genshin@hotmail.com>
License: GPLv3
--]]


--Delay to when handlers update
local update_interval = 25

local particle_emitter_interval = 4

--Delay to when ambiance will loop
local ambiance_interval = 200
local ambiance_list = {}

local debug = minetest.settings:get_bool("enable_portals_debug") or false

local function get_count_from_table(table)
  count = 0
  for k,v in pairs(table) do
    count = count + 1
  end
  return count
end



local function do_particle_effect(pos, delay, amount, texture, min_size, max_size, radius, gravity, glow)

	radius = radius or 2
	min_size = min_size or 0.5
	max_size = max_size or 1
	gravity = gravity or -10
	glow = glow or 0
        delay = delay or 0.25

	minetest.add_particlespawner({
		amount = amount,
		time = delay,
		minpos = pos,
		maxpos = pos,
		minvel = {x = -radius, y = -radius, z = -radius},
		maxvel = {x = radius, y = radius, z = radius},
		minacc = {x = 0, y = gravity, z = 0},
		maxacc = {x = 0, y = gravity, z = 0},
		minexptime = 0.1,
		maxexptime = 1,
		minsize = min_size,
		maxsize = max_size,
		texture = texture,
		glow = glow,
	})
end


local function update_ambiance_to_players_within_radius(self, distance)
  local pos = self.object:get_pos()
  local node = self.portal_nodename
  local ambiance_sound = get_portal_handler_sounds(node, "portal_ambiance")
  local ambiance_distance = get_portal_handler_sounds(node, "sound_distance")
  local entities = minetest.get_objects_inside_radius(pos, ambiance_distance)
  local players = minetest.get_connected_players()
  local within_radius = {}

  --Get the following players within the radius and stack them in a table
  for k,v in pairs(entities) do
    local entity = v

    if not entity then
       within_radius = {}
      break
    end

    if entity:is_player() then
      local name = entity:get_player_name()

      within_radius[name] = entity

    end

  end


  --Check which players are ouside of radius
  for _,player in pairs(players) do 
  local name = player:get_player_name()

    if within_radius[name] then

      if not ambiance_list[name] then

        local ambiance = minetest.sound_play(ambiance_sound, {
	    object = player,
	    loop = true,
        })

        ambiance_list[name] = ambiance

      end

    else

    if ambiance_list[name] then 
      minetest.sound_stop(ambiance_list[name])
      ambiance_list[name] = nil
    end

    end

  end

end


local function force_stop_ambiance_to_players()
  local players = minetest.get_connected_players()

  --Check which players are ouside of radius
  for _,player in pairs(players) do
    local name = player:get_player_name()

    if name then

      if ambiance_list[name] then
        minetest.sound_stop(ambiance_list[name])
        ambiance_list[name] = nil
      end

    end

  end

end


--[Local Function] Teleport Player or Mob to a location or pparticle_emitter_intervalortal
local function teleport_entity(userdata, self)
  local portal_data = get_portal_data(self.portal)
  local linked = portal_data.linked
  local linked_portal_data = nil
  local pos = nil
  local height = nil
  local node = self.portal_nodename
  local warp_sound = get_portal_handler_sounds(node, "portal_warp") or ""
  local sound_distance = get_portal_handler_sounds(node, "sound_distance")
  local entity_pos = userdata:get_pos()

  if linked == true then
    linked_portal_data = get_portal_data(portal_data.linked_portal)

    if not linked_portal_data then
     return
    end 

    pos = linked_portal_data.location
    --height = get_portal_handler_origin_height(self.portal_nodename, "warp_handler")

    if pos == nil then
      minetest.log("error", "Failed to find location of linked portal. Refusing to teleport entity to a unknown location")
      return
    end
    
    if debug == true then
    minetest.chat_send_all("Teleported Something! - Linked")
    end
 
    do_particle_effect(entity_pos, 0.25, 1, "warp_particle.png", 10, 20, radius, 20, 30)
    userdata:set_pos({x = pos.x, y = pos.y + 3, z = pos.z})

  elseif linked == false then
    pos = portal_data.coordinates

    if pos == nil then
      minetest.log("error", "Failed to find specified destination. Refusing to teleport entity to a unknown location")
      return
    end

    if debug == true then
    minetest.chat_send_all("Teleported Something! - Not Linked")
    end

    do_particle_effect(entity_pos, 5, 25, "warp_particle.png", math.random(5,10), math.random(10,20), radius, 20, 10)
    userdata:set_pos(pos)
  end

  if userdata:is_player() then
 
    local name = userdata:get_player_name()

    if ambiance_list[name] then 
      minetest.sound_stop(ambiance_list[name])
      ambiance_list[name] = nil
    end

  end

  minetest.sound_play(warp_sound, {
    pos = self.object:get_pos(),
    max_hear_distance = sound_distance,
    gain = 10.0,
  })

  minetest.sound_play(warp_sound, {
    pos = pos,
    max_hear_distance = sound_distance,
    gain = 10.0,
  })

  return
end


--[Local Function] Get node from vertical position
local function get_node_from_vertical_pos(userdata, direction, vertical_height)
  local pos = userdata:get_pos()


  if direction == nil then else
    if direction == "up" then
      pos = {x = pos.x, y = pos.y + vertical_height, z = pos.z}
    elseif direction == "down" then
      pos = {x = pos.x, y = pos.y - vertical_height, z = pos.z}
    end
  end

  local result = tostring(minetest.get_node(pos)["name"]) or "Error"

  return result
end



--[Local Function] Portal Warp Handler Behavior
local function handler_step(self)

  self.update_timer = self.update_timer + 1

  if self.emit_particles == "true" then

  self.particle_emitter_timer = self.particle_emitter_timer + 1

  if self.particle_emitter_timer >= particle_emitter_interval then
  local texture = get_portal_particle_values(self.portal_nodename, "texture")
  local delay = get_portal_particle_values(self.portal_nodename, "delay")
  local spread = get_portal_particle_values(self.portal_nodename, "spread")

  local pos = self.object:get_pos()
  do_particle_effect(pos, delay, 1, texture, math.random(5,8), math.random(8,12), spread, 1, 30)
  self.particle_emitter_timer = 0
  end

  end


  if self.update_timer >= update_interval then

    if self.origin_height == nil then
      force_stop_ambiance_to_players()
      self.object:remove()
      return
    end

    local portal_node = tostring(get_node_from_vertical_pos(self.object, "down", self.origin_height))

    --minetest.chat_send_all(tostring(portal_node)..", "..tostring(self.portal_node)..", "..tostring(self.origin_height)..", "..tostring(self.portal))

    if self.portal == nil or self.portal_node == nil then --delete yourself if it's not associated with a portal (failsafe)
      force_stop_ambiance_to_players()
      --minetest.chat_send_all("Uh Oh 2 - Handler")
      self.object:remove()
      return

    elseif portal_node ~= self.portal_node then --No Portal below you?, if so then kill yourself (failsafe)
      force_stop_ambiance_to_players()
      --minetest.chat_send_all("Uh Oh 3 - Handler")
      self.object:remove()
      return
    end

      update_ambiance_to_players_within_radius(self)

    local portal_data = get_portal_data(self.portal)
    
    --If data hasn't loaded successfully, skip the rest of the stuff (failsafe)
    if portal_data == nil then
      return
    end

    self.emit_particles = get_portal_particle_values(self.portal_nodename, "flag") or self.emit_particles

    local pos = self.object:get_pos()
    local detected_entities = minetest.get_objects_inside_radius(pos, self.scan_radius)
    local private = portal_data.private

    if private == true then
      local whitelist = portal_data.whitelist

      for _,entity in pairs(detected_entities) do

        if entity:is_player() then

          if debug == true then
          minetest.chat_send_all("Attempting to teleport someone! - Private")
          end

          local player = entity
          local player_name = player:get_player_name()

          for k,v in pairs(whitelist) do
            local allowed = v
            local pname = player:get_player_name()

            if pname == allowed or player_name == self.portal_owner then
              teleport_entity(player, self)
            end

          end

        else

          local npc = entity:get_luaentity()

          if npc.type and npc ~= self then
            if debug == true then
            minetest.chat_send_all("Attempting to teleport something! - Private")
            end

            teleport_entity(npc.object, self)
          end

        end

      end

    elseif private == false then

      for _,entity in pairs(detected_entities) do

        if entity:is_player() then

          if debug == true then
          minetest.chat_send_all("Attempting to teleport someone! - Not Private")
          end

          teleport_entity(entity, self)
        else
          local npc = entity:get_luaentity()

          if npc.type and npc ~= self then

          if debug == true then
            minetest.chat_send_all("Attempting to teleport something! - Not Private")
          end

            teleport_entity(npc.object, self)
          end

        end


      end


    end

   ::portal_handler_loop_end::
    self.update_timer = 0

  end

end


local function update_portal_nametag(self)

  local portal_data = get_portal_data(self.portal)
    
  --If data hasn't loaded successfully, skip the rest of the stuff (failsafe)
  if portal_data == nil then
    return
  end

    self.object:set_properties({
      nametag = minetest.colorize("#0066ff", portal_data.nametag).."\n"..minetest.colorize("#959595", "["..portal_data.portal_name.."]")
    })

end


--[Local Function] Portal Nametag Behavior
local function nametag_step(self)


  self.update_timer = self.update_timer + 1

  if self.update_timer >= update_interval then

    if self.origin_height == nil then
      --minetest.chat_send_all("Uh Oh 1 - Nametag")
      self.object:remove()
      return
    end

    local portal_node = tostring(get_node_from_vertical_pos(self.object, "down", self.origin_height))

    --minetest.chat_send_all(tostring(portal_node)..", "..tostring(self.portal_node)..", "..tostring(self.origin_height)..", "..tostring(self.portal))

    if self.portal == nil or self.portal_node == nil then --delete yourself if it's not associated with a portal (failsafe)
      --minetest.chat_send_all("Uh Oh 2 - Nametag")
      self.object:remove()
      return

    elseif portal_node ~= self.portal_node then --No Portal below you?, if so then kill yourself (failsafe)
      --minetest.chat_send_all("Uh Oh 3 - Nametag")
      self.object:remove()
      return
    end

    local portal_data = get_portal_data(self.portal)
    
    --If data hasn't loaded successfully, skip the rest of the stuff (failsafe)
    if portal_data == nil then
      return
    end

    update_portal_nametag(self)

    self.update_timer = 0

  end

end


--[Entity] Portal Teleportation Handler
minetest.register_entity("advanced_portals:portal_handler", {
    physical = false,
    collisionbox = {0, 0, 0, 0, 0, 0},
    visual = "sprite",
    glow = 30,
    textures = {"portal_deactivated.png"},
    visual_size = {x = 0, y = 0},
    origin_height = nil,
    portal = nil,
    portal_node = nil,
    portal_nodename = nil,
    scan_radius = 2,
    update_timer = 0,
    particle_emitter_timer = 0,
    emit_particles = "false",
    particle_texture = "",
    particle_delay = 0,
    particle_spread = 0,

    on_activate = function(self, staticdata, dtime_s)
      -- load entity variables
      local tmp = minetest.deserialize(staticdata)

      if tmp then

        for _,stat in pairs(tmp) do
          self[_] = stat
        end
      end

      self.set = self.set
      self.radius = self.radius
      self.object:set_armor_groups({immortal = 100}) --Can't die by anything
      self.object:set_properties(self)


      if debug == true then
      self.object:set_properties({
        nametag = "<[DEBUG]: Portal Handler Position>"
      })
      end

      if self.portal == nil then else

      local portal_data = get_portal_data(self.portal)
      local current_origin_height = get_portal_handler_origin_height(self.portal_nodename, "warp_handler")

      --print("[DEBUG] warp handler current_origin_height = "..current_origin_height)

        if current_origin_height ~= self.origin_height then
          local pos = portal_data.location
          self.origin_height = current_origin_height
          self.object:set_pos({x = pos.x, y = pos.y + current_origin_height, z = pos.z})
        end
  
      end


    end,

    get_staticdata = function(self)

      local tmp = {}

      for _,stat in pairs(self) do

        local t = type(stat)

        if  t ~= 'function'
        and t ~= 'nil'
        and t ~= 'userdata' then
          tmp[_] = self[_]
        end
      end

      return minetest.serialize(tmp)
    end,

    on_punch = function(self, hitter) --Can't get punched by anything
      return
    end,

    on_step = function(self) 
      handler_step(self)
    end
})


--[Entity] Portal Nametag
minetest.register_entity("advanced_portals:portal_nametag", {
    physical = false,
    collisionbox = {0, 0, 0, 0, 0, 0},
    visual = "sprite",
    tag = "",
    glow = 30,
    textures = {"portal_deactivated.png"},
    visual_size = {x = 0, y = 0},
    origin_height = nil,
    portal = nil,
    set = false,
    portal_node = nil,
    portal_nodename = nil,
    scan_radius = 2,
    update_timer = 0,

    on_activate = function(self, staticdata, dtime_s)
      -- load entity variables
      local tmp = minetest.deserialize(staticdata)
    
      if tmp then

        for _,stat in pairs(tmp) do
          self[_] = stat
        end
      end

      self.radius = self.radius
      self.object:set_armor_groups({immortal = 100}) --Can't die by anything
      self.object:set_properties(self)
      update_portal_nametag(self)

      if self.portal == nil then else

        local portal_data = get_portal_data(self.portal)
        local current_origin_height = get_portal_handler_origin_height(self.portal_nodename, "nametag")

        --print("[DEBUG] nametag current_origin_height = "..current_origin_height)

        if current_origin_height ~= self.origin_height then
          local pos = portal_data.location
          self.origin_height = current_origin_height
          self.object:set_pos({x = pos.x, y = pos.y + current_origin_height, z = pos.z})
        end

      end

    end,

    get_staticdata = function(self)

      local tmp = {}

      for _,stat in pairs(self) do

        local t = type(stat)

        if  t ~= 'function'
        and t ~= 'nil'
        and t ~= 'userdata' then
          tmp[_] = self[_]
            end
      end

      return minetest.serialize(tmp)
    end,

    on_punch = function(self, hitter) --Can't get punched by anything
      return
    end,

    on_step = function(self) 
    nametag_step(self)
    end,

})
